package com.dbflow5.processor.definition;

import com.dbflow5.MapUtils;
import com.dbflow5.StringUtils;
import com.dbflow5.annotation.*;
import com.dbflow5.processor.ClassNames;
import com.dbflow5.processor.MethodDefinition;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.Validators;
import com.dbflow5.processor.definition.behavior.Behaviors;
import com.dbflow5.processor.definition.behavior.ColumnBehaviors;
import com.dbflow5.processor.definition.behavior.CreationQueryBehavior;
import com.dbflow5.processor.definition.behavior.FTSBehaviors;
import com.dbflow5.processor.definition.column.ColumnDefinition;
import com.dbflow5.processor.definition.column.DefinitionUtils;
import com.dbflow5.processor.definition.column.ReferenceColumnDefinition;
import com.dbflow5.processor.utils.ElementUtility;
import com.dbflow5.processor.utils.ModelUtils;
import com.dbflow5.processor.utils.ProcessorUtils;
import com.squareup.javapoet.*;

import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * Description: Used in writing ModelAdapters
 */
public class TableDefinition extends EntityDefinition {
    private final Table table;

    public enum Type {
        Normal,
        FTS3,
        FTS4;

        boolean isVirtual() {
            return this != Normal;
        }
    }

    public String insertConflictActionName = "";

    public String updateConflictActionName = "";

    public String primaryKeyConflictActionName = "";

    public List<ColumnDefinition> _primaryColumnDefinitions = new ArrayList<>();
    public List<ReferenceColumnDefinition> foreignKeyDefinitions = new ArrayList<>();
    public List<ReferenceColumnDefinition> columnMapDefinitions = new ArrayList<>();
    public List<UniqueGroupsDefinition> uniqueGroupsDefinitions = new ArrayList<>();
    public List<IndexGroupsDefinition> indexGroupsDefinitions = new ArrayList<>();

    public boolean implementsContentValuesListener;

    public boolean implementsSqlStatementListener;

    public TableDefinition(Table table, ProcessorManager manager, TypeElement element) {
        super(element, manager);
        this.table = table;

        creationQueryBehavior = new CreationQueryBehavior(table.createWithDatabase());
        useIsForPrivateBooleans = table.useBooleanGetterSetters();
        generateContentValues = table.generateContentValues();

        //init
        setOutputClassName("_Table");

        manager.addModelToDatabase(elementClassName, associationalBehavior().databaseTypeName);

        Fts4 fts4 = element.getAnnotation(Fts4.class);
        Fts3 fts3 = element.getAnnotation(Fts3.class);

        if (fts3 != null && fts4 != null) {
            manager.logError("Table " + elementClassName + " cannot have multiple FTS annotations.");
        }

        if (fts4 != null) {
            type = Type.FTS4;
        } else if (fts3 != null) {
            type = Type.FTS3;
        } else {
            type = Type.Normal;
        }

        if (type == Type.FTS4) {
            ftsBehavior = new FTSBehaviors.FTS4Behavior(ProcessorUtils.extractTypeNameFromAnnotation(fts4, e -> null, it -> {
                it.contentTable();
                return null;
            }), associationalBehavior().databaseTypeName, elementName, manager);
        } else if (type == Type.FTS3) {
            ftsBehavior = new FTSBehaviors.FTS3Behavior(elementName, manager);
        } else {
            ftsBehavior = null;
        }

        InheritedColumn[] inheritedColumns = table.inheritedColumns();
        for (InheritedColumn it : inheritedColumns) {
            if (inheritedFieldNameList.contains(it.fieldName())) {
                manager.logError("A duplicate inherited column with name " + it.fieldName() + " " +
                        "was found for " + associationalBehavior().name);
            }
            inheritedFieldNameList.add(it.fieldName());
            inheritedColumnMap.put(it.fieldName(), it);
        }

        InheritedPrimaryKey[] inheritedPrimaryKeys = table.inheritedPrimaryKeys();
        for (InheritedPrimaryKey it : inheritedPrimaryKeys) {
            if (inheritedFieldNameList.contains(it.fieldName())) {
                manager.logError("A duplicate inherited column with name ${it.fieldName} " +
                        "was found for " + associationalBehavior().name);
            }
            inheritedFieldNameList.add(it.fieldName());
            inheritedPrimaryKeyMap.put(it.fieldName(), it);
        }

        implementsContentValuesListener = ProcessorUtils.implementsClass(element, manager.processingEnvironment,
                ClassNames.CONTENT_VALUES_LISTENER);

        implementsSqlStatementListener = ProcessorUtils.implementsClass(element, manager.processingEnvironment,
                ClassNames.SQLITE_STATEMENT_LISTENER);

        contentValueMethods = new MethodDefinition[]{new Methods.BindToContentValuesMethod(this, true, implementsContentValuesListener),
                new Methods.BindToContentValuesMethod(this, false, implementsContentValuesListener)};

        temporary = table.temporary();

        cachingBehavior = new Behaviors.CachingBehavior(
                table.cachingEnabled(),
                table.cacheSize(),
                null,
                null);
    }

    @Override
    public MethodDefinition[] methods() {
        return new MethodDefinition[]{
                new Methods.BindToStatementMethod(this, Methods.BindToStatementMethod.Mode.INSERT),
                new Methods.BindToStatementMethod(this, Methods.BindToStatementMethod.Mode.UPDATE),
                new Methods.BindToStatementMethod(this, Methods.BindToStatementMethod.Mode.DELETE),
                new Methods.InsertStatementQueryMethod(this, Methods.InsertStatementQueryMethod.Mode.INSERT),
                new Methods.InsertStatementQueryMethod(this, Methods.InsertStatementQueryMethod.Mode.SAVE),
                new Methods.UpdateStatementQueryMethod(this),
                new Methods.DeleteStatementQueryMethod(this),
                new Methods.CreationQueryMethod(this),
                new Methods.LoadFromCursorMethod(this),
                new Methods.ExistenceMethod(this),
                new Methods.PrimaryConditionMethod(this),
                new Methods.OneToManyDeleteMethod(this, false),
                new Methods.OneToManyDeleteMethod(this, true),
                new Methods.OneToManySaveMethod(this, Methods.OneToManySaveMethod.METHOD_SAVE, false),
                new Methods.OneToManySaveMethod(this, Methods.OneToManySaveMethod.METHOD_SAVE, true),
                new Methods.OneToManySaveMethod(this, Methods.OneToManySaveMethod.METHOD_INSERT, false),
                new Methods.OneToManySaveMethod(this, Methods.OneToManySaveMethod.METHOD_INSERT, true),
                new Methods.OneToManySaveMethod(this, Methods.OneToManySaveMethod.METHOD_UPDATE, false),
                new Methods.OneToManySaveMethod(this, Methods.OneToManySaveMethod.METHOD_UPDATE, true)
        };
    }

    private final MethodDefinition[] contentValueMethods;

    private final CreationQueryBehavior creationQueryBehavior;
    public boolean useIsForPrivateBooleans;
    private final boolean generateContentValues;

    public List<OneToManyDefinition> oneToManyDefinitions = new ArrayList<>();

    private final Map<String, ColumnDefinition> columnMap = new LinkedHashMap<>();
    private final Map<Integer, Set<ColumnDefinition>> columnUniqueMap = new LinkedHashMap<>();
    private final Map<String, InheritedColumn> inheritedColumnMap = new HashMap<>();
    private final List<String> inheritedFieldNameList = new ArrayList<>();
    private final Map<String, InheritedPrimaryKey> inheritedPrimaryKeyMap = new HashMap<>();

    public boolean hasPrimaryConstructor = false;

    private Behaviors.AssociationalBehavior associationalBehavior;
    private Behaviors.CursorHandlingBehavior cursorHandlingBehavior;

    @Override
    public Behaviors.AssociationalBehavior associationalBehavior() {
        if (associationalBehavior == null) {
            associationalBehavior = new Behaviors.AssociationalBehavior(
                    StringUtils.isNullOrEmpty(table.name()) ? element.getSimpleName().toString() : table.name(),
                    ProcessorUtils.extractTypeNameFromAnnotation(table, e -> null, it -> {
                        it.database();
                        return null;
                    }), table.allFields());
        }
        return associationalBehavior;
    }

    @Override
    public Behaviors.CursorHandlingBehavior cursorHandlingBehavior() {
        if (cursorHandlingBehavior == null) {
            cursorHandlingBehavior = new Behaviors.CursorHandlingBehavior(
                    table.orderedCursorLookUp(),
                    table.assignDefaultValuesFromCursor());
        }
        return cursorHandlingBehavior;
    }

    public Behaviors.CachingBehavior cachingBehavior;

    public Type type;

    public FTSBehaviors.FtsBehavior ftsBehavior;

    public boolean temporary;

    @Override
    public void prepareForWriteInternal() {
        columnMap.clear();
        _primaryColumnDefinitions.clear();
        uniqueGroupsDefinitions.clear();
        indexGroupsDefinitions.clear();
        foreignKeyDefinitions.clear();
        columnMapDefinitions.clear();
        columnUniqueMap.clear();
        oneToManyDefinitions.clear();
        cachingBehavior.clear();

        // globular default
        ConflictAction insertConflict = table.insertConflict();
        if (insertConflict == ConflictAction.NONE && databaseDefinition().insertConflict != ConflictAction.NONE) {
            insertConflict = databaseDefinition().insertConflict;
        }

        ConflictAction updateConflict = table.updateConflict();
        if (updateConflict == ConflictAction.NONE && databaseDefinition().updateConflict != ConflictAction.NONE) {
            updateConflict = databaseDefinition().updateConflict;
        }

        ConflictAction primaryKeyConflict = table.primaryKeyConflict();

        insertConflictActionName = insertConflict == ConflictAction.NONE ? "" : insertConflict.name();
        updateConflictActionName = updateConflict == ConflictAction.NONE ? "" : updateConflict.name();
        primaryKeyConflictActionName = primaryKeyConflict == ConflictAction.NONE ? "" : primaryKeyConflict.name();

        if (typeElement != null) {
            createColumnDefinitions(typeElement);
        }

        UniqueGroup[] groups = table.uniqueColumnGroups();
        Set<Integer> uniqueNumbersSet = new HashSet<>();
        for (UniqueGroup uniqueGroup : groups) {
            if (uniqueNumbersSet.contains(uniqueGroup.groupNumber())) {
                manager.logError("A duplicate unique group with number" +
                        " " + uniqueGroup.groupNumber() + " was found for " + associationalBehavior.name);
            }
            UniqueGroupsDefinition definition = new UniqueGroupsDefinition(uniqueGroup);
            for (ColumnDefinition it : columnDefinitions) {
                if (it.uniqueGroups.contains(definition.number)) {
                    definition.addColumnDefinition(it);
                }
            }

            uniqueGroupsDefinitions.add(definition);
            uniqueNumbersSet.add(uniqueGroup.groupNumber());
        }

        IndexGroup[] indexGroups = table.indexGroups();
        uniqueNumbersSet = new HashSet<>();
        for (IndexGroup indexGroup : indexGroups) {
            if (uniqueNumbersSet.contains(indexGroup.number())) {
                manager.logError(TableDefinition.class, "A duplicate unique index number" +
                        " " + indexGroup.number() + " was found for " + elementName);
            }
            IndexGroupsDefinition definition = new IndexGroupsDefinition(this, indexGroup);
            for (ColumnDefinition it : columnDefinitions) {
                if (it.indexGroups.contains(definition.indexNumber)) {
                    definition.columnDefinitionList.add(it);
                }
            }

            indexGroupsDefinitions.add(definition);
            uniqueNumbersSet.add(indexGroup.number());
        }
    }

    @Override
    public void createColumnDefinitions(TypeElement typeElement) {
        List<Element> elements = ElementUtility.getAllElements(typeElement, manager);

        for (Element element : elements) {
            classElementLookUpMap.put(element.getSimpleName().toString(), element);
            if (element instanceof ExecutableElement && ((ExecutableElement) element).getParameters().isEmpty()
                    && element.getSimpleName().toString().equals("<init>")
                    && element.getEnclosingElement() == typeElement
                    && !element.getModifiers().contains(Modifier.PRIVATE)) {
                hasPrimaryConstructor = true;
            }
        }

        if (!hasPrimaryConstructor) {
            manager.logError("For now, tables must have a visible, default, parameterless constructor. In" +
                    " Kotlin all field parameters must have default values.");
        }

        Validators.ColumnValidator columnValidator = new Validators.ColumnValidator();
        Validators.OneToManyValidator oneToManyValidator = new Validators.OneToManyValidator();
        elements.forEach(variableElement -> {
            // no private static or final fields for all columns, or any inherited columns here.
            boolean isAllFields = ElementUtility.isValidAllFields(associationalBehavior.allFields, variableElement);

            // package private, will generate helper
            boolean isPackagePrivate = ElementUtility.isPackagePrivate(variableElement);
            boolean isPackagePrivateNotInSamePackage = isPackagePrivate && !ElementUtility.isInSamePackage(manager, variableElement, this.element);

            boolean isForeign = variableElement.getAnnotation(ForeignKey.class) != null;
            boolean isPrimary = variableElement.getAnnotation(PrimaryKey.class) != null;
            boolean isInherited = inheritedColumnMap.containsKey(variableElement.getSimpleName().toString());
            boolean isInheritedPrimaryKey = inheritedPrimaryKeyMap.containsKey(variableElement.getSimpleName().toString());
            boolean isColumnMap = variableElement.getAnnotation(ColumnMap.class) != null;
            if (variableElement.getAnnotation(Column.class) != null || isForeign || isPrimary
                    || isAllFields || isInherited || isInheritedPrimaryKey || isColumnMap) {

                if (checkInheritancePackagePrivate(isPackagePrivateNotInSamePackage, variableElement)) return;

                ColumnDefinition columnDefinition;
                if (isInheritedPrimaryKey) {
                    InheritedPrimaryKey inherited = inheritedPrimaryKeyMap.get(variableElement.getSimpleName().toString());
                    columnDefinition = new ColumnDefinition(manager, variableElement, this, isPackagePrivateNotInSamePackage,
                            inherited != null ? inherited.column() : null, inherited != null ? inherited.primaryKey() : null, ConflictAction.NONE);
                } else if (isInherited) {
                    InheritedColumn inherited = inheritedColumnMap.get(variableElement.getSimpleName().toString());
                    columnDefinition = new ColumnDefinition(manager, variableElement, this, isPackagePrivateNotInSamePackage,
                            inherited != null ? inherited.column() : null, null, inherited != null ? inherited.nonNullConflict() : ConflictAction.NONE);
                } else if (isForeign) {
                    columnDefinition = new ReferenceColumnDefinition(variableElement.getAnnotation(ForeignKey.class), manager, this,
                            variableElement, isPackagePrivateNotInSamePackage);
                } else if (isColumnMap) {
                    columnDefinition = new ReferenceColumnDefinition(variableElement.getAnnotation(ColumnMap.class),
                            manager, this, variableElement, isPackagePrivateNotInSamePackage);
                } else {
                    columnDefinition = new ColumnDefinition(manager, variableElement,
                            this, isPackagePrivateNotInSamePackage, element.getAnnotation(Column.class), element.getAnnotation(PrimaryKey.class), ConflictAction.NONE);
                }

                if (columnValidator.validate(manager, columnDefinition)) {
                    columnDefinitions.add(columnDefinition);
                    if (isPackagePrivate) {
                        packagePrivateList.add(columnDefinition);
                    }
                    columnMap.put(columnDefinition.columnName, columnDefinition);
                    // check to ensure not null.
                    if (columnDefinition.type instanceof ColumnDefinition.Type.Primary) {
                        _primaryColumnDefinitions.add(columnDefinition);
                    } else if (columnDefinition.type instanceof ColumnDefinition.Type.PrimaryAutoIncrement) {
                        this.primaryKeyColumnBehavior = new ColumnBehaviors.PrimaryKeyColumnBehavior(false, columnDefinition, true);
                    } else if (columnDefinition.type instanceof ColumnDefinition.Type.RowId) {
                        this.primaryKeyColumnBehavior = new ColumnBehaviors.PrimaryKeyColumnBehavior(true, columnDefinition, false);
                    }

                    if (primaryKeyColumnBehavior.associatedColumn != null) {
                        // check to ensure not null.
                        if (primaryKeyColumnBehavior.associatedColumn.isNullableType) {
                            manager.logWarning("Attempting to use nullable field type on an autoincrementing column. " +
                                    "To suppress or remove this warning " +
                                    "switch to java primitive, add @ohos.support.annotation.NonNull," +
                                    "@org.jetbrains.annotation.NotNull, or in Kotlin don't make it nullable. Check the column " + primaryKeyColumnBehavior.associatedColumn.columnName + " " +
                                    "on ${associationalBehavior.name}");
                        }
                    }

                    if (ftsBehavior != null) {
                        ftsBehavior.validateColumnDefinition(columnDefinition);
                    }

                    if (columnDefinition instanceof ReferenceColumnDefinition) {
                        if (!((ReferenceColumnDefinition) columnDefinition).isColumnMap()) {
                            foreignKeyDefinitions.add((ReferenceColumnDefinition) columnDefinition);
                        } else {
                            columnMapDefinitions.add((ReferenceColumnDefinition) columnDefinition);
                        }
                    }

                    if (!columnDefinition.uniqueGroups.isEmpty()) {
                        for (Integer group : columnDefinition.uniqueGroups) {
                            MapUtils.getOrPut(columnUniqueMap, group, new LinkedHashSet<>())
                                    .add(columnDefinition);
                        }
                    }
                }
            } else if (variableElement.getAnnotation(OneToMany.class) != null) {
                OneToManyDefinition oneToManyDefinition = new OneToManyDefinition((ExecutableElement) variableElement, manager, elements);
                if (oneToManyValidator.validate(manager, oneToManyDefinition)) {
                    oneToManyDefinitions.add(oneToManyDefinition);
                }
            } else {
                cachingBehavior.evaluateElement(variableElement, typeElement, manager);
            }
        });

        // ignore any referenced one to many field definitions from all fields.
        columnDefinitions = columnDefinitions.stream().filter(column -> oneToManyDefinitions.isEmpty() || oneToManyDefinitions.stream().noneMatch(it -> it.variableName.equals(column.elementName))).collect(Collectors.toList());
    }

    @Override
    public List<ColumnDefinition> primaryColumnDefinitions() {
        if (primaryKeyColumnBehavior().associatedColumn != null) {
            return Collections.singletonList(primaryKeyColumnBehavior().associatedColumn);
        } else {
            if (ftsBehavior != null) {
                return columnDefinitions;
            } else {
                return _primaryColumnDefinitions;
            }
        }
    }

    @Override
    public TypeName extendsClass() {
        return ParameterizedTypeName.get(ClassNames.MODEL_ADAPTER, elementClassName);
    }

    @Override
    public void onWriteDefinition(TypeSpec.Builder typeBuilder) {
        // check references to properly set them up.
        foreignKeyDefinitions.forEach(ReferenceColumnDefinition::checkNeedsReferences);
        columnMapDefinitions.forEach(ReferenceColumnDefinition::checkNeedsReferences);

        writeGetModelClass(typeBuilder, elementClassName);
        this.writeConstructor(typeBuilder);
        associationalBehavior.writeName(typeBuilder);

        typeBuilder.addMethod(MethodSpec
                .methodBuilder("getType")
                .returns(ClassNames.OBJECT_TYPE)
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addStatement("return $T.Table", ClassNames.OBJECT_TYPE)
                .build());

        if (StringUtils.isNotNullOrEmpty(updateConflictActionName)) {
            typeBuilder.addMethod(MethodSpec
                    .methodBuilder("getUpdateOnConflictAction")
                    .returns(ClassNames.CONFLICT_ACTION)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addStatement("return $T." + updateConflictActionName, ClassNames.CONFLICT_ACTION)
                    .build());

        }

        if (StringUtils.isNotNullOrEmpty(insertConflictActionName)) {
            typeBuilder.addMethod(MethodSpec
                    .methodBuilder("getInsertOnConflictAction")
                    .returns(ClassNames.CONFLICT_ACTION)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addStatement("return $T." + insertConflictActionName, ClassNames.CONFLICT_ACTION)
                    .build());
        }

        String paramColumnName = "columnName";
        CodeBlock.Builder getPropertiesBuilder = CodeBlock.builder();

        MethodSpec.Builder methodBuilder = MethodSpec
                .methodBuilder("getProperty")
                .returns(ClassNames.PROPERTY)
                .addParameter(ParameterSpec.builder(String.class, paramColumnName).build())
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addStatement("String " + paramColumnName + "2 = $T.quoteIfNeeded(" + paramColumnName + ")", ClassNames.STRING_UTILS);

        methodBuilder.beginControlFlow("switch (" + "(" + paramColumnName + "2)" + ")");

        for (int i = 0; i < columnDefinitions.size(); i++) {
            if (i > 0) {
                getPropertiesBuilder.add(",");
            }
            ColumnDefinition columnDefinition = columnDefinitions.get(i);
            if (elementClassName != null) {
                columnDefinition.addPropertyDefinition(typeBuilder, elementClassName);
            }
            columnDefinition.addPropertyCase(methodBuilder);
            columnDefinition.addColumnName(getPropertiesBuilder);
        }

        methodBuilder.beginControlFlow("default:").addStatement("throw new $T(\"Invalid column name passed. Ensure you are calling the correct table's column\")", IllegalArgumentException.class).endControlFlow();
        methodBuilder.endControlFlow();

        typeBuilder.addMethod(methodBuilder.build());


        typeBuilder.addField(FieldSpec.builder(ArrayTypeName.of(ClassNames.IPROPERTY), "ALL_COLUMN_PROPERTIES")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                .initializer(CodeBlock.builder().add("new $T[]{$L}", ClassNames.IPROPERTY, getPropertiesBuilder.build().toString()).build())
                .build());

        // add index properties here
        for (IndexGroupsDefinition indexGroupsDefinition : indexGroupsDefinitions) {
            typeBuilder.addField(indexGroupsDefinition.fieldSpec());
        }

        if (primaryKeyColumnBehavior.hasAutoIncrement || primaryKeyColumnBehavior.hasRowID) {
            ColumnDefinition autoIncrement = primaryKeyColumnBehavior.associatedColumn;
            if (autoIncrement != null) {
                typeBuilder.addMethod(MethodSpec
                        .methodBuilder("updateAutoIncrement")
                        .returns(TypeName.VOID)
                        .addParameter(ParameterSpec.builder(elementClassName, ModelUtils.variable).build())
                        .addParameter(ParameterSpec.builder(Number.class, "id").build())
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        .addCode(autoIncrement.updateAutoIncrementMethod())
                        .build());
            }
        }

        List<ReferenceColumnDefinition> saveForeignKeyFields = new ArrayList<>();
        columnDefinitions.forEach(it -> {
            if ((it instanceof ReferenceColumnDefinition)
                    && ((ReferenceColumnDefinition) it).foreignKeyColumnBehavior != null
                    && ((ReferenceColumnDefinition) it).foreignKeyColumnBehavior.saveForeignKeyModel) {
                saveForeignKeyFields.add((ReferenceColumnDefinition) it);
            }
        });

        if (!saveForeignKeyFields.isEmpty()) {
            CodeBlock.Builder code = CodeBlock.builder();
            saveForeignKeyFields.forEach(it -> it.appendSaveMethod(code));

            typeBuilder.addMethod(MethodSpec
                    .methodBuilder("saveForeignKeys")
                    .returns(TypeName.VOID)
                    .addParameter(ParameterSpec.builder(elementClassName, ModelUtils.variable).build())
                    .addParameter(ClassNames.DATABASE_WRAPPER, ModelUtils.wrapper)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addCode(code.build())
                    .build());
        }

        List<ReferenceColumnDefinition> deleteForeignKeyFields = new ArrayList<>();
        columnDefinitions.forEach(it -> {
            if ((it instanceof ReferenceColumnDefinition)
                    && ((ReferenceColumnDefinition) it).foreignKeyColumnBehavior != null
                    && ((ReferenceColumnDefinition) it).foreignKeyColumnBehavior.deleteForeignKeyModel) {
                deleteForeignKeyFields.add((ReferenceColumnDefinition) it);
            }
        });

        if (!deleteForeignKeyFields.isEmpty()) {
            CodeBlock.Builder code = CodeBlock.builder();
            deleteForeignKeyFields.forEach(it -> it.appendDeleteMethod(code));

            typeBuilder.addMethod(MethodSpec
                    .methodBuilder("deleteForeignKeys")
                    .returns(TypeName.VOID)
                    .addParameter(ParameterSpec.builder(elementClassName, ModelUtils.variable).build())
                    .addParameter(ClassNames.DATABASE_WRAPPER, ModelUtils.wrapper)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addCode(code.build())
                    .build());
        }

        typeBuilder.addMethod(MethodSpec
                .methodBuilder("getAllColumnProperties")
                .returns(ArrayTypeName.of(ClassNames.IPROPERTY))
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addStatement("return ALL_COLUMN_PROPERTIES")
                .build());

        creationQueryBehavior.addToType(typeBuilder);

        if (cachingBehavior.cachingEnabled) {
            int customCacheSize = cachingBehavior.customCacheSize;
            String customCacheFieldName = cachingBehavior.customCacheFieldName;
            String customMultiCacheFieldName = cachingBehavior.customMultiCacheFieldName;

            FieldSpec.Builder fieldBuilder = FieldSpec
                    .builder(ClassNames.CACHE_ADAPTER, "cacheAdapter")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL);

            CodeBlock.Builder codeBuilder = CodeBlock.builder();

            List<ColumnDefinition> primaryColumns = primaryColumnDefinitions();

            boolean hasCustomField = StringUtils.isNotNullOrEmpty(customCacheFieldName);
            boolean hasCustomMultiCacheField = StringUtils.isNotNullOrEmpty(customMultiCacheFieldName);
            List<Object> typeClasses = new ArrayList<>();
            String typeArgumentsString;
            if (hasCustomField) {
                typeClasses.add(elementClassName);
                typeArgumentsString = "$T." + customCacheFieldName;
            } else {
                typeClasses.add(ClassNames.SIMPLE_MAP_CACHE);
                typeArgumentsString = "new $T(" + customCacheSize + ")";
            }
            typeArgumentsString += ", " + primaryColumns.size();
            if (hasCustomMultiCacheField) {
                typeClasses.add(elementClassName);
                typeArgumentsString += ", $T." + customMultiCacheFieldName;
            } else {
                typeArgumentsString += ", null";
            }

            TypeSpec.Builder typeSpec = TypeSpec.anonymousClassBuilder(typeArgumentsString, typeClasses)
                    .addSuperinterface(ParameterizedTypeName.get(ClassNames.CACHE_ADAPTER, elementTypeName));

            if (primaryColumns.size() > 1) {
                MethodSpec.Builder methodSpecBuilder = MethodSpec
                        .methodBuilder("getCachingColumnValuesFromModel")
                        .returns(ArrayTypeName.of(Object.class))
                        .addParameter(ParameterSpec.builder(ArrayTypeName.of(Object.class), "inValues").build())
                        .addParameter(ParameterSpec.builder(elementClassName, ModelUtils.variable).build())
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL);

                for (int i = 0; i < primaryColumns.size(); i++) {
                    ColumnDefinition column = primaryColumns.get(i);
                    methodSpecBuilder.addCode(column.getColumnAccessString(i));
                }

                methodBuilder.addStatement("return inValues");

                typeSpec.addMethod(methodSpecBuilder.build());


                MethodSpec.Builder methodSpecBuilder2 = MethodSpec
                        .methodBuilder("getCachingColumnValuesFromCursor")
                        .returns(ArrayTypeName.of(Object.class))
                        .addParameter(ParameterSpec.builder(ArrayTypeName.of(Object.class), "inValues").build())
                        .addParameter(ParameterSpec.builder(ClassNames.FLOW_CURSOR, "cursor").build())
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL);

                for (int i = 0; i < primaryColumns.size(); i++) {
                    ColumnDefinition column = primaryColumns.get(i);
                    String method = DefinitionUtils.getLoadFromCursorMethodString(column.elementTypeName, column.complexColumnBehavior.wrapperTypeName);
                    methodSpecBuilder2.addStatement("inValues[" + i + "] = " + Methods.LoadFromCursorMethod.PARAM_CURSOR +
                            "." + method + "(" + Methods.LoadFromCursorMethod.PARAM_CURSOR + ".getColumnIndex(\"" + column.columnName + "\"))");
                }
                methodSpecBuilder2.addStatement("return inValues");

                typeSpec.addMethod(methodSpecBuilder2.build());
            } else {
                // single primary key
                typeBuilder.addMethod(MethodSpec
                        .methodBuilder("getCachingColumnValueFromModel")
                        .returns(Object.class)
                        .addParameter(ParameterSpec.builder(elementClassName, ModelUtils.variable).build())
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        .addCode(primaryColumns.get(0).getSimpleAccessString())
                        .build());

                ColumnDefinition column = primaryColumns.get(0);
                String method = DefinitionUtils.getLoadFromCursorMethodString(column.elementTypeName, column.complexColumnBehavior.wrapperTypeName);
                typeBuilder.addMethod(MethodSpec
                        .methodBuilder("getCachingColumnValueFromCursor")
                        .returns(Object.class)
                        .addParameter(ParameterSpec.builder(ClassNames.FLOW_CURSOR, "cursor").build())
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        .addStatement("return " + Methods.LoadFromCursorMethod.PARAM_CURSOR + "." + method + "(" + Methods.LoadFromCursorMethod.PARAM_CURSOR + ".getColumnIndex(\"" + column.columnName + "\"))")
                        .build());

                typeBuilder.addMethod(MethodSpec
                        .methodBuilder("getCachingId")
                        .returns(Object.class)
                        .addParameter(ParameterSpec.builder(elementClassName, ModelUtils.variable).build())
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        .addStatement("return " + "getCachingColumnValueFromModel(" + ModelUtils.variable + ")")
                        .build());
            }

            if (!foreignKeyDefinitions.isEmpty()) {
                CodeBlock.Builder codeBuilder2 = CodeBlock.builder();

                AtomicInteger noIndex = new AtomicInteger(-1);
                NameAllocator nameAllocator = new NameAllocator();
                foreignKeyDefinitions.forEach(it -> codeBuilder2.add(it.getLoadFromCursorMethod(false, noIndex, nameAllocator)));

                typeBuilder.addMethod(MethodSpec
                        .methodBuilder("reloadRelationships")
                        .returns(TypeName.VOID)
                        .addParameter(ParameterSpec.builder(elementClassName, ModelUtils.variable).build())
                        .addParameter(ParameterSpec.builder(ClassNames.FLOW_CURSOR, Methods.LoadFromCursorMethod.PARAM_CURSOR).build())
                        .addParameter(ParameterSpec.builder(ClassNames.DATABASE_WRAPPER, ModelUtils.wrapper).build())
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        .addCode(codeBuilder2.build())
                        .build());
            }

            codeBuilder.add("$L", typeSpec.build());

            fieldBuilder.initializer(codeBuilder.build());
            typeBuilder.addField(fieldBuilder.build());


            boolean singlePrimaryKey = primaryColumnDefinitions().size() == 1;

            typeBuilder.addMethod(MethodSpec
                    .methodBuilder("createSingleModelLoader")
                    .returns(ClassNames.SINGLE_MODEL_LOADER)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addStatement("return new $T<>(getTable(), cacheAdapter)", singlePrimaryKey ? ClassNames.SINGLE_KEY_CACHEABLE_MODEL_LOADER : ClassNames.CACHEABLE_MODEL_LOADER)
                    .build());

            typeBuilder.addMethod(MethodSpec
                    .methodBuilder("createListModelLoader")
                    .returns(ClassNames.LIST_MODEL_LOADER)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addStatement("return " + "new $T<>(getTable(), cacheAdapter)", singlePrimaryKey ? ClassNames.SINGLE_KEY_CACHEABLE_LIST_MODEL_LOADER : ClassNames.CACHEABLE_LIST_MODEL_LOADER)
                    .build());

            typeBuilder.addMethod(MethodSpec
                    .methodBuilder("createListModelSaver")
                    .returns(ParameterizedTypeName.get(ClassNames.CACHEABLE_LIST_MODEL_SAVER, elementClassName))
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PROTECTED)
                    .addStatement("return " + "new $T<>(getModelSaver(), cacheAdapter)", ClassNames.CACHEABLE_LIST_MODEL_SAVER)
                    .build());

            typeBuilder.addMethod(MethodSpec
                    .methodBuilder("cachingEnabled")
                    .returns(TypeName.BOOLEAN)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addStatement("return " + true)
                    .build());

            typeBuilder.addMethod(MethodSpec
                    .methodBuilder("load")
                    .returns(elementClassName)
                    .addParameter(ParameterSpec.builder(elementClassName, "model").build())
                    .addParameter(ParameterSpec.builder(ClassNames.DATABASE_WRAPPER, ModelUtils.wrapper).build())
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addStatement("$T loaded = super.load(model, " + ModelUtils.wrapper + ")", elementClassName)
                    .addStatement("cacheAdapter.storeModelInCache(model)")
                    .addStatement("return loaded")
                    .build());

        }

        MethodDefinition[] methods = methods();
        for (MethodDefinition definition : methods) {
            if (definition != null) {
                typeBuilder.addMethod(definition.methodSpec());
            }
        }

        if (generateContentValues) {
            for (MethodDefinition definition : contentValueMethods) {
                if (definition != null) {
                    typeBuilder.addMethod(definition.methodSpec());
                }
            }
        }
    }
}
